
package com.ndp.fb.fbagent.service.webrequest;

import com.ndp.fb.config.ConfigCentre;
import com.ndp.fb.fbagent.service.help.CheckAccessTokenHelper;
import com.ndp.fb.redis.api.RedisClient;
import com.ndp.fb.util.StringUtil;
import com.restfb.BinaryAttachment;
import com.restfb.WebRequestor;
import com.restfb.exception.FacebookJsonMappingException;
import com.restfb.json.JsonException;
import com.restfb.json.JsonObject;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import static com.restfb.util.StringUtils.*;
import static com.restfb.util.UrlUtils.urlDecode;
import static java.lang.String.format;
import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.util.logging.Level.*;

/**
 * Default implementation of a service that sends HTTP requests to the Facebook API endpoint.
 *
 * @author <a href="http://restfb.com">Mark Allen</a>
 */
public class QuartzWebRequestor implements WebRequestor {
    /**
     * Arbitrary unique boundary marker for multipart {@code POST}s.
     */
    private static final String MULTIPART_BOUNDARY = "**boundarystringwhichwill**neverbeencounteredinthewild**";

    /**
     * Line separator for multipart {@code POST}s.
     */
    private static final String MULTIPART_CARRIAGE_RETURN_AND_NEWLINE = "\r\n";

    /**
     * Hyphens for multipart {@code POST}s.
     */
    private static final String MULTIPART_TWO_HYPHENS = "--";

    /**
     * Default buffer size for multipart {@code POST}s.
     */
    private static final int MULTIPART_DEFAULT_BUFFER_SIZE = 8192;

    /**
     * By default, how long should we wait for a response (in ms)?
     */
    private static final int DEFAULT_READ_TIMEOUT_IN_MS = 180000;
    protected static final String ERROR_ATTRIBUTE_NAME = "error";
    /**
     * API error response 'code' attribute name.
     */
    protected static final String ERROR_CODE_ATTRIBUTE_NAME = "code";

    /**
     * API error response 'error_subcode' attribute name.
     */
    protected static final String ERROR_SUBCODE_ATTRIBUTE_NAME = "error_subcode";
    /**
     * Batch API error response 'error_description' attribute name.
     */
    protected static final String BATCH_ERROR_DESCRIPTION_ATTRIBUTE_NAME = "error_description";
    /**
     * Logger.
     */
    private static final Logger logger = Logger.getLogger("com.restfb.HTTP");
    private static final org.slf4j.Logger log = LoggerFactory.getLogger("com.ndpmedia.pmd.fb.apilimit");
    private static final org.slf4j.Logger loge = LoggerFactory.getLogger("com.ndpmedia.pmd.fb.apilimitexception");
    private final org.slf4j.Logger logx = LoggerFactory.getLogger(this.getClass());
    /**
     * By default this is true, to prevent breaking existing usage
     */
    private boolean autocloseBinaryAttachmentStream = true;
//  private Map<String, Object> = new Ha
    /**
     * @see com.restfb.WebRequestor#executeGet(String)
     */
    String accountId;

    // Added for ETag
    private String etagKey;

    private RedisClient redisClient;

    private CheckAccessTokenHelper checkAccessTokenHelper;

    /**
     * 增加如果返回的json 不是完整的json传，增加重试一次
     *
     * @param url
     * @return
     * @throws IOException
     */
    public Response executeGet(String url) throws IOException {
        logx.debug("url >>>>> " + url);
        if (logger.isLoggable(FINE))
            logger.fine("Making a GET request to " + url);
        String result = checkAccessTokenHelper.checkAccessToken(url);
        if (!com.restfb.util.StringUtils.isBlank(result)) {
            Response response = new WebRequestor.Response(200, result);
            return response;
        }
        HttpURLConnection httpUrlConnection = null;
        InputStream inputStream = null;
        Long time = System.currentTimeMillis();
        try {
            httpUrlConnection = openConnection(new URL(url));
            httpUrlConnection.setReadTimeout(DEFAULT_READ_TIMEOUT_IN_MS);
            httpUrlConnection.setUseCaches(false);

            // Allow subclasses to customize the connection if they'd like to - set
            // their own headers, timeouts, etc.
            customizeConnection(httpUrlConnection);

            httpUrlConnection.setRequestMethod("GET");

            // Added for ETag
            if (StringUtils.isNotEmpty(etagKey)) {  // 如果设定了eTagKey，则从Redis中查找ETag
                Object eTag = redisClient.get(etagKey);
                if (null != eTag) { // 如果ETag不为空则添加到RequestHeader中
                    Boolean useEtag = ConfigCentre.getBoolean("switch.use.etag");
                    if (useEtag) {
                    	logx.info("使用eTag进行查询, url>>>" + url + ", eTag>>>" + eTag.toString());
                        httpUrlConnection.setRequestProperty("If-None-Match", eTag.toString());
                    }
                }
            }
            httpUrlConnection.connect();

            Map<String, List<String>> headerFields = httpUrlConnection.getHeaderFields();
            if (logger.isLoggable(FINER)) {
                logger.finer("Response headers: " + headerFields);
            }

            int responseCode = httpUrlConnection.getResponseCode();

            // Added for ETag
            if (StringUtils.isNotEmpty(etagKey) && HTTP_OK == responseCode) {  // 如果设定了eTagKey，并且返回结果为200，则需要缓存新的ETag
                List<String> etag = headerFields.get("ETag");
                if (!CollectionUtils.isEmpty(etag)) {   // 如果返回的Header中有eTag字段则将其缓存
                    Long expire = redisClient.getExpire(etagKey, TimeUnit.SECONDS);
                    if (0 == expire) {   // 第一次设定该Key，过期时间为28天，即86400秒
                        expire = 3600 * 24 * 28L;
                    }
                    redisClient.set(etagKey, etag.get(0), expire, TimeUnit.SECONDS.name());
                }
            }

            try {
                inputStream =
                        responseCode != HTTP_OK ? httpUrlConnection.getErrorStream() : httpUrlConnection
                                .getInputStream();
            } catch (IOException e) {
                if (logger.isLoggable(WARNING))
                    logger.warning(format("An error occurred while making a GET request to %s: %s", url, e));
            }

            String json = fromInputStream(inputStream);
            logx.debug("fb result >>>> " + json);
            if (null == json && HTTP_NOT_MODIFIED == responseCode) {    // 处理304特殊情况，将返回结果修改为空字符串，以免调用的地方出现空指针
                json = "";
            } else if (StringUtil.isNotEmpty(json) && json.startsWith("{")) { //判断是否是完整的json传，如果不是以 { 开始 }结束的，增加一次重试
                if (!(json.trim().endsWith("}"))) {
                    logx.error("facebook请求获取的json 不完整，重试：" + json);
                    closeQuietly(httpUrlConnection);
                    httpUrlConnection.connect();
                    responseCode = httpUrlConnection.getResponseCode();
                    try {
                        inputStream = responseCode != HTTP_OK ? httpUrlConnection.getErrorStream() : httpUrlConnection.getInputStream();
                    } catch (IOException e) {
                        if (logger.isLoggable(WARNING))
                            logger.warning(format("Redo An error occurred while making a GET request to %s: %s", url, e));
                    }
                    json = fromInputStream(inputStream);
                }
            }

            Response response = new Response(responseCode, json);
            String traceId = httpUrlConnection.getHeaderField("x-fb-debug");
            String debugId = httpUrlConnection.getHeaderField("x-fb-trace-id");
            log.debug("time-_-" + time + "-_-url-_-" + url + "-_-fbaccountid-_-" + accountId+ "-_-traceid-_-" + traceId+ "-_-debugid-_-" + debugId);
            if (logger.isLoggable(FINE))
                logger.fine("Facebook responded with " + response);
            
            throwFacebookResponseLimitExceptionIfNecessary(url, time, json, responseCode,traceId,debugId);
            return checkAccessTokenHelper.checkFBResponse(response, url, null);
        } finally {
            closeQuietly(httpUrlConnection);
        }
    }

    /**
     * @see com.restfb.WebRequestor#executePost(String, String)
     */

    @Override
    public Response executePost(String url, String parameters) throws IOException {
        Response response = executePost(url, parameters, (BinaryAttachment[]) null);
        return checkAccessTokenHelper.checkFBResponse(response, url, parameters);
    }

    /**
     * @see com.restfb.WebRequestor#executePost(String, String, com.restfb.BinaryAttachment[])
     */

    @Override
    public Response executePost(String url, String parameters, BinaryAttachment... binaryAttachments) throws IOException {
        String result = checkAccessTokenHelper.checkAccessToken(url,parameters);
        if (!com.restfb.util.StringUtils.isBlank(result)) {
            Response response = new WebRequestor.Response(200, result);
            return response;
        }
        if (binaryAttachments == null)
            binaryAttachments = new BinaryAttachment[]{};

        if (logger.isLoggable(FINE))
            logger.fine("Executing a POST to " + url + " with parameters "
                    + (binaryAttachments.length > 0 ? "" : "(sent in request body): ") + urlDecode(parameters)
                    + (binaryAttachments.length > 0 ? " and " + binaryAttachments.length + " binary attachment[s]." : ""));

        HttpURLConnection httpUrlConnection = null;
        OutputStream outputStream = null;
        InputStream inputStream = null;
        Long time = System.currentTimeMillis();
        try {
            httpUrlConnection = openConnection(new URL(url + (binaryAttachments.length > 0 ? "?" + parameters : "")));
            httpUrlConnection.setReadTimeout(DEFAULT_READ_TIMEOUT_IN_MS);

            // Allow subclasses to customize the connection if they'd like to - set
            // their own headers, timeouts, etc.
            customizeConnection(httpUrlConnection);

            httpUrlConnection.setRequestMethod("POST");
            httpUrlConnection.setDoOutput(true);
            httpUrlConnection.setUseCaches(false);

            if (binaryAttachments.length > 0) {
                httpUrlConnection.setRequestProperty("Connection", "Keep-Alive");
                httpUrlConnection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + MULTIPART_BOUNDARY);
            }

            // Added for ETag
            if (StringUtils.isNotEmpty(etagKey)) {  // 如果设定了eTagKey，则从Redis中查找ETag
                Object eTag = redisClient.get(etagKey);
                if (null != eTag) { // 如果ETag不为空则添加到RequestHeader中
                    Boolean useEtag = ConfigCentre.getBoolean("switch.use.etag");
                    if (useEtag) {
                    	logx.info("使用eTag进行查询, url>>>" + url + ", eTag>>>" + eTag.toString());
                        httpUrlConnection.setRequestProperty("If-None-Match", eTag.toString());
                    }
                }
            }

            httpUrlConnection.connect();
            outputStream = httpUrlConnection.getOutputStream();

            // If we have binary attachments, the body is just the attachments and the
            // other parameters are passed in via the URL.
            // Otherwise the body is the URL parameter string.
            if (binaryAttachments.length > 0) {
                for (BinaryAttachment binaryAttachment : binaryAttachments) {
                    StringBuilder stringBuilder = new StringBuilder();

                    stringBuilder.append(MULTIPART_TWO_HYPHENS).append(MULTIPART_BOUNDARY)
                            .append(MULTIPART_CARRIAGE_RETURN_AND_NEWLINE).append("Content-Disposition: form-data; name=\"")
                            .append(createFormFieldName(binaryAttachment)).append("\"; filename=\"")
                            .append(binaryAttachment.getFilename()).append("\"");

                    if (binaryAttachment.getContentType() != null && binaryAttachment.getContentType().length() > 0)
                        stringBuilder.append(MULTIPART_CARRIAGE_RETURN_AND_NEWLINE).append("Content-Type: ")
                                .append(binaryAttachment.getContentType());

                    stringBuilder.append(MULTIPART_CARRIAGE_RETURN_AND_NEWLINE).append(MULTIPART_CARRIAGE_RETURN_AND_NEWLINE);

                    outputStream.write(stringBuilder.toString().getBytes(ENCODING_CHARSET));

                    write(binaryAttachment.getData(), outputStream, MULTIPART_DEFAULT_BUFFER_SIZE);

                    outputStream.write((MULTIPART_CARRIAGE_RETURN_AND_NEWLINE + MULTIPART_TWO_HYPHENS + MULTIPART_BOUNDARY
                            + MULTIPART_TWO_HYPHENS + MULTIPART_CARRIAGE_RETURN_AND_NEWLINE).getBytes(ENCODING_CHARSET));
                }
            } else {
                outputStream.write(parameters.getBytes(ENCODING_CHARSET));
            }

            Map<String, List<String>> headerFields = httpUrlConnection.getHeaderFields();
            if (logger.isLoggable(FINER)) {
                logger.finer("Response headers: " + headerFields);
            }

            int responseCode = httpUrlConnection.getResponseCode();

            // Added for ETag
            if (StringUtils.isNotEmpty(etagKey) && HTTP_OK == responseCode) {  // 如果设定了eTagKey，并且返回结果为200，则需要缓存新的ETag
                List<String> etag = headerFields.get("ETag");
                if (!CollectionUtils.isEmpty(etag)) {   // 如果返回的Header中有eTag字段则将其缓存
                    Long expire = redisClient.getExpire(etagKey, TimeUnit.SECONDS);
                    if (0 == expire) {   // 第一次设定该Key，过期时间为28天，即86400秒
                        expire = 3600 * 24 * 28L;
                    }
                    redisClient.set(etagKey, etag.get(0), expire, TimeUnit.SECONDS.name());
                }
            }

            try {
                inputStream =
                        responseCode != HTTP_OK ? httpUrlConnection.getErrorStream() : httpUrlConnection
                                .getInputStream();
            } catch (IOException e) {
                if (logger.isLoggable(WARNING))
                    logger.warning("An error occurred while POSTing to " + url + ": " + e);
            }
            String json = fromInputStream(inputStream);
            logx.debug("fb返回 >>>> " + json);
            if (null == json && HTTP_NOT_MODIFIED == responseCode) {    // 处理304特殊情况，将返回结果修改为空字符串，以免调用的地方出现空指针
                json = "";
            }
            Response response = new Response(responseCode, json);
            String traceId = httpUrlConnection.getHeaderField("x-fb-debug");
            String debugId = httpUrlConnection.getHeaderField("x-fb-trace-id");
            log.debug("time-_-" + time + "-_-url-_-" + url + "-_-fbaccountid-_-" + accountId+ "-_-traceid-_-" + traceId+ "-_-debugid-_-" + debugId);
            if (logger.isLoggable(FINE))
                logger.fine("Facebook responded with " + response);
            throwFacebookResponseLimitExceptionIfNecessary(url+"?"+parameters, time, json, responseCode,traceId,debugId);
            return response;
        } finally {
            if (autocloseBinaryAttachmentStream && binaryAttachments.length > 0)
                for (BinaryAttachment binaryAttachment : binaryAttachments)
                    closeQuietly(binaryAttachment.getData());

            closeQuietly(outputStream);
            closeQuietly(httpUrlConnection);
        }
    }

    /**
     * Given a {@code url}, opens and returns a connection to it.
     * <p/>
     * If you'd like to pipe your connection through a proxy, this is the place to do so.
     *
     * @param url The URL to connect to.
     * @return A connection to the URL.
     * @throws java.io.IOException If an error occurs while establishing the connection.
     * @since 1.6.3
     */
    protected HttpURLConnection openConnection(URL url) throws IOException {
        return (HttpURLConnection) url.openConnection();
    }

    /**
     * Hook method which allows subclasses to easily customize the {@code connection}s created by
     * {@link #executeGet(String)} and {@link #executePost(String, String)} - for example, setting a custom read timeout
     * or request header.
     * <p/>
     * This implementation is a no-op.
     *
     * @param connection The connection to customize.
     */
    protected void customizeConnection(HttpURLConnection connection) {
    }

    /**
     * Attempts to cleanly close a resource, swallowing any exceptions that might occur since there's no way to recover
     * anyway.
     * <p/>
     * It's OK to pass {@code null} in, this method will no-op in that case.
     *
     * @param closeable The resource to close.
     */
    protected void closeQuietly(Closeable closeable) {
        if (closeable == null)
            return;
        try {
            closeable.close();
        } catch (Throwable t) {
            if (logger.isLoggable(WARNING))
                logger.warning("Unable to close " + closeable + ": " + t);
        }
    }

    /**
     * Attempts to cleanly close an {@code HttpURLConnection}, swallowing any exceptions that might occur since there's no
     * way to recover anyway.
     * <p/>
     * It's OK to pass {@code null} in, this method will no-op in that case.
     *
     * @param httpUrlConnection The connection to close.
     */
    protected void closeQuietly(HttpURLConnection httpUrlConnection) {
        if (httpUrlConnection == null)
            return;
        try {
            httpUrlConnection.disconnect();
        } catch (Throwable t) {
            if (logger.isLoggable(WARNING))
                logger.warning("Unable to disconnect " + httpUrlConnection + ": " + t);
        }
    }

    /**
     * Writes the contents of the {@code source} stream to the {@code destination} stream using the given
     * {@code bufferSize}.
     *
     * @param source      The source stream to copy from.
     * @param destination The destination stream to copy to.
     * @param bufferSize  The size of the buffer to use during the copy operation.
     * @throws java.io.IOException  If an error occurs when reading from {@code source} or writing to {@code destination}.
     * @throws NullPointerException If either {@code source} or @{code destination} is {@code null}.
     */
    protected void write(InputStream source, OutputStream destination, int bufferSize) throws IOException {
        if (source == null || destination == null)
            throw new NullPointerException("Must provide non-null source and destination streams.");

        int read = 0;
        byte[] chunk = new byte[bufferSize];
        while ((read = source.read(chunk)) > 0)
            destination.write(chunk, 0, read);
    }

    /**
     * Creates the form field name for the binary attachment filename by stripping off the file extension - for example,
     * the filename "test.png" would return "test".
     *
     * @param binaryAttachment The binary attachment for which to create the form field name.
     * @return The form field name for the given binary attachment.
     */
    protected String createFormFieldName(BinaryAttachment binaryAttachment) {
        String name = binaryAttachment.getFilename();
        int fileExtensionIndex = name.lastIndexOf(".");
        return fileExtensionIndex > 0 ? name.substring(0, fileExtensionIndex) : name;
    }

    /**
     * returns if the binary attachment stream is closed automatically
     *
     * @return
     * @since 1.7.0
     */
    public boolean isAutocloseBinaryAttachmentStream() {
        return autocloseBinaryAttachmentStream;
    }

    /**
     * define if the binary attachment stream is closed automatically after sending the content to facebook
     *
     * @param autocloseBinaryAttachmentStream
     * @since 1.7.0
     */
    public void setAutocloseBinaryAttachmentStream(boolean autocloseBinaryAttachmentStream) {
        this.autocloseBinaryAttachmentStream = autocloseBinaryAttachmentStream;
    }

    protected void throwFacebookResponseLimitExceptionIfNecessary(String url, Long time, String json, Integer httpStatusCode,String traceId,String debugId) {
        try {
            // If the result is not an object, bail immediately.
            if (!json.startsWith("{"))
                return;

            int subStrEnd = Math.min(50, json.length());
            if (!json.substring(0, subStrEnd).contains("\"error\"")) {
                return; // no need to parse json
            }

            JsonObject errorObject = new JsonObject(json);

            if (errorObject == null || !errorObject.has(ERROR_ATTRIBUTE_NAME))
                return;

            JsonObject innerErrorObject = errorObject.getJsonObject(ERROR_ATTRIBUTE_NAME);

            // If there's an Integer error code, pluck it out.
            Integer errorCode =
                    innerErrorObject.has(ERROR_CODE_ATTRIBUTE_NAME) ? toInteger(innerErrorObject
                            .getString(ERROR_CODE_ATTRIBUTE_NAME)) : null;
            Integer errorSubcode =
                    innerErrorObject.has(ERROR_SUBCODE_ATTRIBUTE_NAME) ? toInteger(innerErrorObject
                            .getString(ERROR_SUBCODE_ATTRIBUTE_NAME)) : null;

            loge.debug("time-_-" + time + "-_-Exception-_-errorCode-_-" + errorCode + "-_-errorSubcode-_-" + errorSubcode + "-_-url-_-" + url + "-_-fbaccountid-_-" + accountId+ "-_-traceid-_-" + traceId+ "-_-debugid-_-" + debugId);
        } catch (JsonException e) {
            throw new FacebookJsonMappingException("Unable to process the Facebook API response", e);
        }
    }

    public String getAccountId() {
        return accountId;
    }

    public void setAccountId(String accountId) {
        this.accountId = accountId;
    }

    public String getEtagKey() {
        return etagKey;
    }

    public void setEtagKey(String etagKey) {
        this.etagKey = etagKey;
    }

    public RedisClient getRedisClient() {
        return redisClient;
    }

    public void setRedisClient(RedisClient redisClient) {
        this.redisClient = redisClient;
    }

    public CheckAccessTokenHelper getCheckAccessTokenHelper() {
        return checkAccessTokenHelper;
    }

    public void setCheckAccessTokenHelper(CheckAccessTokenHelper checkAccessTokenHelper) {
        this.checkAccessTokenHelper = checkAccessTokenHelper;
    }
}
